/*
 * Project: Set Sample
 * Copyright (C) 2013 alf.labs gmail com,
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.alflabs.setsample.model;

import com.alflabs.annotations.NonNull;
import com.alflabs.annotations.Null;
import com.alflabs.setsample.stats.SetStats;
import com.alflabs.serial.SerialReader;
import com.alflabs.serial.SerialWriter;
import com.alflabs.setsample.presenter.DummyAbstractView;
import com.alflabs.setsample.presenter.IAbstractView;

import java.util.Collections;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;

public class SetModel {
    public final static boolean DEBUG = false;

    private long mModelSeed;
    private long mCounter;
    private boolean mCompleted;
    private boolean mReadyForNewGame;
    @NonNull private IAbstractView mAbstractView = new DummyAbstractView();
    private final SetBoard mBoard = new SetBoard();
    private final SetStats mStats = new SetStats();
    private final Set<SetSolution> mPossibleSolutions = new TreeSet<SetSolution>();
    private final Set<SetSolution> mUserSolutions = new TreeSet<SetSolution>();
    private final Set<SetSolution> mPossibleSolutions_RO = Collections.unmodifiableSet(mPossibleSolutions);
    private final Set<SetSolution> mUserSolutions_RO = Collections.unmodifiableSet(mUserSolutions);

    public SetModel() {
    }

    public SetStats getStats() {
        return mStats;
    }

    public long getSeed() {
        return mModelSeed;
    }

    public long getCounter() {
        return mCounter;
    }

    public long incCounter() {
        return ++mCounter;
    }

    public boolean isCompleted() {
        return mCompleted;
    }

    public boolean isReadyForNewGame() {
        return mReadyForNewGame;
    }

    public void setReadyForNewGame(boolean readyForNewGame) {
        mReadyForNewGame = readyForNewGame;
    }

    public void setAbstractView(@Null IAbstractView abstractView) {
        if (abstractView == null) {
            abstractView =  new DummyAbstractView();
        }
        mAbstractView = abstractView;
    }

    @NonNull
    public IAbstractView getAbstractView() {
        return mAbstractView;
    }

    @NonNull
    public SetBoard getBoard() {
        return mBoard;
    }

    /** Returns the unmodified set of possible solutions. Never null. */
    @NonNull
    public Set<SetSolution> getPossibleSolutions() {
        return mPossibleSolutions_RO;
    }

    /** Returns the unmodified set of users solutions. Never null. */
    @NonNull
    public Set<SetSolution> getUserSolutions() {
        return mUserSolutions_RO;
    }

    /**
     * Adds a (potentially new) user solution.
     * If the solution is new, the user set is modified, the internal counter is incremented and
     * {@link com.alflabs.setsample.presenter.IAbstractView#resetSolutions()} is called.
     *
     * @param s A non-null user solution.
     * @return True if the solution wasn't already in the user list; false if it was already present.
     */
    public boolean addUserSolution(@NonNull SetSolution s) {
        if (mUserSolutions.contains(s)) {
            return false;
        } else {
            mUserSolutions.add(s);
            incCounter();
            mAbstractView.resetSolutions();

            if (mPossibleSolutions.size() == mUserSolutions.size()) {
                mCompleted = true;
            }

            return true;
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        SetModel setModel = (SetModel) o;

        if (mCompleted != setModel.mCompleted) return false;
        if (mReadyForNewGame != setModel.mReadyForNewGame) return false;
        if (mCounter != setModel.mCounter) return false;
        if (mModelSeed != setModel.mModelSeed) return false;
        if (!mBoard.equals(setModel.mBoard)) return false;
        if (!mPossibleSolutions.equals(setModel.mPossibleSolutions)) return false;
        if (!mStats.equals(setModel.mStats)) return false;
        //noinspection RedundantIfStatement
        if (!mUserSolutions.equals(setModel.mUserSolutions)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = (int) (mModelSeed ^ (mModelSeed >>> 32));
        result = 31 * result + (int) (mCounter ^ (mCounter >>> 32));
        result = 31 * result + (mCompleted ? 1 : 0);
        result = 31 * result + (mReadyForNewGame ? 1 : 0);
        result = 31 * result + mBoard.hashCode();
        result = 31 * result + mStats.hashCode();
        result = 31 * result + mPossibleSolutions.hashCode();
        result = 31 * result + mUserSolutions.hashCode();
        return result;
    }

    public void randomizeNewGame(Random rnd) {
        mPossibleSolutions.clear();
        mUserSolutions.clear();
        mCompleted = false;
        mReadyForNewGame = false;
        mModelSeed = rnd.nextLong();
        SetSolver.initBoard4(mModelSeed, mBoard, null /*mPossibleSolutions*/);
        mPossibleSolutions.addAll(SetSolver.solveBoard(mBoard));
        incCounter();
        mAbstractView.resetBoard();
        mAbstractView.resetSolutions();
    }

    public SerialWriter toSerial() {
        SerialWriter sw = new SerialWriter();

        sw.addLong("mSeed",    mModelSeed);
        sw.addLong("mCounter", mCounter);
        sw.addBool("mCompleted", mCompleted);
        sw.addBool("mReadyForNewGame", mReadyForNewGame);
        sw.addSerial("mBoard", mBoard.toSerial());

        _setToSerial(sw, "mPossibleSolutions", mPossibleSolutions);
        _setToSerial(sw, "mUserSolutions", mUserSolutions);

        return sw;
    }

    private void _setToSerial(SerialWriter sw, String name, Set<SetSolution> set) {
        sw.addInt(name + "_len", set.size());
        int k = 0;
        for (SetSolution s : set) {
            String sn = name + k;
            k++;
            sw.addInt(sn + "_0", s.mCards[0].hashCode());
            sw.addInt(sn + "_1", s.mCards[1].hashCode());
            sw.addInt(sn + "_2", s.mCards[2].hashCode());
        }
    }

    public void fromSerial(SerialReader sr) {
        mModelSeed = sr.getLong("mSeed");
        mCounter   = sr.getLong("mCounter");
        mCompleted = sr.getBool("mCompleted");
        mReadyForNewGame = sr.getBool("mReadyForNewGame");
        mBoard.fromSerial(sr.getSerial("mBoard"));

        _setFromSerial(sr, "mPossibleSolutions", mPossibleSolutions);
        _setFromSerial(sr, "mUserSolutions", mUserSolutions);
    }

    private void _setFromSerial(SerialReader sr, String name, Set<SetSolution> set) {
        set.clear();
        int len = sr.getInt(name + "_len");
        for (int k = 0; k < len; k++) {
            String sn = name + k;
            SetCard c0 = new SetCard(sr.getInt(sn + "_0"));
            SetCard c1 = new SetCard(sr.getInt(sn + "_1"));
            SetCard c2 = new SetCard(sr.getInt(sn + "_2"));
            set.add(new SetSolution(c0, c1, c2));
        }
    }

    /** String representation for debugging. */
    @Override
    public String toString() {
        return "SetModel{" +
                "mModelSeed=" + mModelSeed +
                ", mCounter=" + mCounter +
                ", mCompleted=" + mCompleted +
                ", mReadyForNewGame=" + mReadyForNewGame +
                ", mBoard=" + mBoard +
                ", mPossibleSolutions=" + mPossibleSolutions +
                ", mUserSolutions=" + mUserSolutions +
                ", mStats=" + mStats +
                '}';
    }
}
